Skip to content

feat: enable server-side Blaze template rendering (foundation for SSG/SSR)#507

Open
dupontbertrand wants to merge 8 commits intometeor:release-3.1.0from
dupontbertrand:feature/static-render
Open

feat: enable server-side Blaze template rendering (foundation for SSG/SSR)#507
dupontbertrand wants to merge 8 commits intometeor:release-3.1.0from
dupontbertrand:feature/static-render

Conversation

@dupontbertrand
Copy link
Copy Markdown

Summary

Enables Blaze.toHTML() to work on the server by making compiled templates and the Template registry available server-side. This is the foundation for SSG/SSR use cases (demonstrated in a working POC — see links below).

The key insight is that Blaze.toHTML() already works server-side through a DOM-free code path — the only problem was that compiled templates and the Template global were restricted to the client. This PR lifts that restriction without breaking anything client-side.

Changes

5 small, backward-compatible changes across 5 packages:

Package Change
templating-compiler Remove archMatching: 'web' so templates compile for all architectures
templating-tools Guard generateBodyJS() with Meteor.isClient — body rendering uses document.body
caching-html-compiler Guard body attrs setter with Meteor.isClient
templating-runtime Export Template to server, guard DOM code with Meteor.isClient
templating Export Template to server (matches templating-runtime)

Why this is safe

  • <template> tag compiled output is pure JavaScript (Template.__checkName + Template[name] = new Template(...)). No DOM access.
  • <body> tag compiled output uses document.body — wrapped in Meteor.isClient to stay client-only.
  • Template.__checkName, Template._migrateTemplate, Blaze.Template constructor are already server-safe (no DOM dependencies).
  • DOM-related code (Template.body, HMR, _applyHmrChanges) wrapped in Meteor.isClient to remain client-only.
  • Server-side overhead: ~500 bytes per compiled template. For an app with 100 templates, ~50 KB of JS loaded at startup.

What this does NOT change

  • Client-side rendering behavior is unchanged
  • blaze, htmljs, spacebars cores are unchanged
  • The DOM-dependent code (materializer, domrange, dombackend, events, attrs) stays client-only

Proof of concept

Built on top of these changes:

Forum discussion: https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556

Testing done

  • Existing Blaze usage (client-side rendering) continues to work unchanged
  • Template.myTemplate is now accessible server-side after import '../lib/templates.html' in server/main.js
  • Blaze.toHTML(Template.myTemplate) returns proper HTML strings server-side
  • Blaze.toHTMLWithData(Template.myTemplate, data) works with data contexts
  • #each, #if, #unless render correctly through _expandView(forExpansion=true)
  • Production meteor build succeeds
  • Vanilla Blaze apps (no SSG/SSR usage) build and run identically

Consumer-side usage

Apps that want to use server-side rendering can now do:

// server/main.js
import '../lib/templates.html';  // makes Template.* available server-side

// later — pure server-side rendering
const html = Blaze.toHTMLWithData(Template.myTemplate, { title: 'Hello' });

Templates must be written as pure render functions against explicit data — no Session, no this.subscribe(), no Template.dynamic (which remains client-only).

…for all architectures

This allows compiled Blaze templates to be available on the server, which
is required for server-side rendering use cases (SSG/SSR).

Server-side overhead is minimal (~500 bytes per template). The <template>
tag compiled output is pure JavaScript with no DOM dependencies. <body>
tag compilation is handled separately in templating-tools.
The <body> tag compiled code calls Template.body.renderToDocument which
uses document.body — this crashes on the server. Wrap generateBodyJS()
output with 'if (Meteor.isClient)' to make body compiled code safe to
load on both architectures.

generateTemplateJS() is unchanged — the <template> tag compiled output
(Template.__checkName + Template[name] = new Template(...)) is already
server-safe.
The generated code calls document.body.setAttribute() which crashes on
the server. Wrap it in 'if (Meteor.isClient)' to make the compiled HTML
safe to load on both architectures.
…teor.isClient

Export Template globally (both client and server) so that compiled
templates can register themselves server-side. Previously Template was
client-only, which prevented any server-side rendering use case.

Template.__checkName is already server-safe (no DOM). Template.body,
Template.__pendingReplacement, Template._applyHmrChanges, and the HMR
branch of Template._migrateTemplate all require the DOM — wrapped with
'if (Meteor.isClient)'.

dynamic.html and dynamic.js remain client-only (they require DOM).
Matches the export change in templating-runtime. The templating package
re-exports Template from templating-runtime and was also restricting it
to the client — which would override the templating-runtime change.
…or.isClient wrap

The simpleBody helper in html-scanner-tests now expects the <body> rendering
code to be wrapped in 'if (Meteor.isClient)', matching the change in
generateBodyJS().
Document the new server-side Blaze rendering capability:
- Blaze.toHTML() / Blaze.toHTMLWithData() work on the server
- How to make templates available server-side (import in server/main.js)
- Template restrictions (no Session, no this.subscribe, no Template.dynamic)
- Link to static-render package for higher-level SSG/SSR API
- Manual integration with server-render package
- Known rspack limitation
dupontbertrand added a commit to dupontbertrand/meteor that referenced this pull request Apr 20, 2026
static-render is a server-side orchestration package that pre-renders
Blaze routes as HTML for SEO. Two modes:

- SSG (Static Site Generation): routes with static: 'ssg' are rendered
  once at server startup and cached permanently. Good for pages that
  rarely change (about, contact, terms).

- SSR (Server-Side Rendering): routes with static: 'ssr' are rendered
  on-the-fly at each request with fresh data from MongoDB via async
  staticData(). Good for product pages, articles, profiles.

Pre-rendered HTML is injected into the Meteor boilerplate via
req.dynamicBody and req.dynamicHead, so the client-side app still
loads and takes over normally (server pre-render + client takeover,
not React-style hydration).

The package auto-discovers routes from flow-router-extra (weak dep).
It also provides graceful error handling: if a template crashes
during rendering, a placeholder comment is rendered and the page
falls back to client-side rendering.

Requires the Blaze 3.1.x+ server-rendering changes (meteor/blaze#507).

Includes:
- packages/static-render/package.js, static-render-server.js, README.md
- v3-docs/docs/packages/static-render.md (full API docs)
- Sidebar entry in v3-docs/.vitepress/config.mts

Forum discussion:
https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556
- Add dedicated SSG subsection with about-page example
- Add dedicated SSR subsection with product-page example
- Add SSG vs SSR comparison table for decision-making
@jankapunkt
Copy link
Copy Markdown
Collaborator

I would target this for release 3.2.0 to keep 3.1.0 manageable and finish it soon

@dupontbertrand
Copy link
Copy Markdown
Author

As you want boss, to be honest it's a "small" changes but if you prefer to wait for 3.2 it's up to you, maybe you have to check with someone else because this PR have to be merged at the same time at this one meteor/meteor#14349

dupontbertrand added a commit to dupontbertrand/meteor that referenced this pull request Apr 20, 2026
static-render is a server-side orchestration package that pre-renders
Blaze routes as HTML for SEO. Two modes:

- SSG (Static Site Generation): routes with static: 'ssg' are rendered
  once at server startup and cached permanently. Good for pages that
  rarely change (about, contact, terms).

- SSR (Server-Side Rendering): routes with static: 'ssr' are rendered
  on-the-fly at each request with fresh data from MongoDB via async
  staticData(). Good for product pages, articles, profiles.

Pre-rendered HTML is injected into the Meteor boilerplate via
req.dynamicBody and req.dynamicHead, so the client-side app still
loads and takes over normally (server pre-render + client takeover,
not React-style hydration).

The package auto-discovers routes from flow-router-extra (weak dep).
It also provides graceful error handling: if a template crashes
during rendering, a placeholder comment is rendered and the page
falls back to client-side rendering.

Requires the Blaze 3.1.x+ server-rendering changes (meteor/blaze#507).

Includes:
- packages/static-render/package.js, static-render-server.js, README.md
- v3-docs/docs/packages/static-render.md (full API docs)
- Sidebar entry in v3-docs/.vitepress/config.mts

Forum discussion:
https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556
dupontbertrand added a commit to dupontbertrand/meteor that referenced this pull request Apr 20, 2026
static-render is a server-side orchestration package that pre-renders
Blaze routes as HTML for SEO. Two modes:

- SSG (Static Site Generation): routes with static: 'ssg' are rendered
  once at server startup and cached permanently. Good for pages that
  rarely change (about, contact, terms).

- SSR (Server-Side Rendering): routes with static: 'ssr' are rendered
  on-the-fly at each request with fresh data from MongoDB via async
  staticData(). Good for product pages, articles, profiles.

Pre-rendered HTML is injected into the Meteor boilerplate via
req.dynamicBody and req.dynamicHead, so the client-side app still
loads and takes over normally (server pre-render + client takeover,
not React-style hydration).

The package auto-discovers routes from flow-router-extra (weak dep).
It also provides graceful error handling: if a template crashes
during rendering, a placeholder comment is rendered and the page
falls back to client-side rendering.

Requires the Blaze 3.1.x+ server-rendering changes (meteor/blaze#507).

Includes:
- packages/static-render/package.js, static-render-server.js, README.md
- v3-docs/docs/packages/static-render.md (full API docs)
- Sidebar entry in v3-docs/.vitepress/config.mts

Forum discussion:
https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556
dupontbertrand added a commit to dupontbertrand/meteor that referenced this pull request Apr 20, 2026
static-render is a server-side orchestration package that pre-renders
Blaze routes as HTML for SEO. Two modes:

- SSG (Static Site Generation): routes with static: 'ssg' are rendered
  once at server startup and cached permanently. Good for pages that
  rarely change (about, contact, terms).

- SSR (Server-Side Rendering): routes with static: 'ssr' are rendered
  on-the-fly at each request with fresh data from MongoDB via async
  staticData(). Good for product pages, articles, profiles.

Pre-rendered HTML is injected into the Meteor boilerplate via
req.dynamicBody and req.dynamicHead, so the client-side app still
loads and takes over normally (server pre-render + client takeover,
not React-style hydration).

The package auto-discovers routes from flow-router-extra (weak dep).
It also provides graceful error handling: if a template crashes
during rendering, a placeholder comment is rendered and the page
falls back to client-side rendering.

Requires the Blaze 3.1.x+ server-rendering changes (meteor/blaze#507).

Includes:
- packages/static-render/package.js, static-render-server.js, README.md
- v3-docs/docs/packages/static-render.md (full API docs)
- Sidebar entry in v3-docs/.vitepress/config.mts

Forum discussion:
https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants